
# Copyright (C) 2008 ZHENG Zhong (heavyzheng at gmail D0T com)
# - http://heavyzheng.blogspot.com/
# - http://buggarden.blogspot.com/
# 
# Last changed at $Date: 2008-06-12 16:44:12 +0000 (Thu, 12 Jun 2008) $ by $Author: heavyzheng $, $Revision: 131 $
# $HeadURL: http://buggarden.googlecode.com/svn/misc/copyrighter.py $


import os
import sys
import re
import logging
from optparse import OptionParser

LOG_FORMAT = "%(asctime)-15s %(levelname)-7s: %(message)s"
logging.basicConfig(format=LOG_FORMAT)


################################################################################
# Copyright classes
#

class CopyrightError(object):
    pass


class Copyright(object):

    COPYRIGHT_PATTERN = r"\s+Copyright \(C\)\s*"

    def __init__(self, lines):
        self.__lines = lines

    def put_into_comment(self, text):
        raise NotImplementedError()
    
    def is_mark(self, line):
        #regex = re.compile(Copyright.COPYRIGHT_PATTERN)
        #return (regex.match(line) is not None)
        pass
    
    def is_begin(self, line):
        #regex = re.compile(Copyright.COPYRIGHT_PATTERN)
        #return (regex.match(line) is not None)
        pass
    
    def is_end(self, line):
        return (line in ("\n", "\r", "\r\n"))

    def as_str(self):
        copyright_str = ""
        for line in self.__lines:
            copyright_str += self.put_into_comment(line) + "\n"
        return copyright_str


class CopyrightForScript(Copyright):
    
    SUPPORTED_EXTENSIONS = ( "py", "txt", )

    def __init__(self, lines):
        super(CopyrightForScript, self).__init__(lines)

    def put_into_comment(self, text):
        return "# " + text

    def is_mark(self, line):
        regex = re.compile(r"^#" + Copyright.COPYRIGHT_PATTERN + r"$")
        return (regex.match(line) is not None)
    
    def is_begin(self, line):
        regex = re.compile(r"^#" + Copyright.COPYRIGHT_PATTERN + r"\w.*$")
        return (regex.match(line) is not None)


class CopyrightForCppOrJava(Copyright):

    SUPPORTED_EXTENSIONS = ( "h", "hpp", "hh", "c", "cpp", "cc", "java", )

    def __init__(self, lines):
        super(CopyrightForCppOrJava, self).__init__(lines)
    
    def put_into_comment(self, text):
        return "// " + text

    def is_mark(self, line):
        regex = re.compile(r"^//" + Copyright.COPYRIGHT_PATTERN + r"$")
        return (regex.match(line) is not None)
    
    def is_begin(self, line):
        regex = re.compile(r"^//" + Copyright.COPYRIGHT_PATTERN + r"\w.*$")
        return (regex.match(line) is not None)


class CopyrightForLaTeX(Copyright):
    
    SUPPORTED_EXTENSIONS = ( "tex", )
    
    def __init__(self, lines):
        super(CopyrightForLaTeX, self).__init__(lines)
    
    def put_into_comment(self, text):
        return "% " + text

    def is_mark(self, line):
        regex = re.compile(r"^%" + Copyright.COPYRIGHT_PATTERN + r"$")
        return (regex.match(line) is not None)
    
    def is_begin(self, line):
        regex = re.compile(r"^%" + Copyright.COPYRIGHT_PATTERN + r"\w.*$")
        return (regex.match(line) is not None)


class CopyrightForML(Copyright):

    SUPPORTED_EXTENSIONS = ( "xml", "xsl", "xsd", "html", "xhtml", )

    def __init__(self, lines):
        super(CopyrightForML, self).__init__(lines)
    
    def put_into_comment(self):
        return "<!-- " + text + " -->"

    def is_mark(self, line):
        regex = re.compile(r"^<!--" + Copyright.COPYRIGHT_PATTERN + r"-->$")
        return (regex.match(line) is not None)
    
    def is_begin(self, line):
        regex = re.compile(r"^<!--" + Copyright.COPYRIGHT_PATTERN + r"\w.*-->$")
        return (regex.match(line) is not None)
        
    def as_str(self):
        copyright_str = "<!--\n"
        for line in self.__lines:
            copyright_str += " " + line
        copyright_str += "-->\n"
        return copyright_str


class CopyrightForCss(Copyright):

    SUPPORTED_EXTENSIONS = ( "css", )

    def __init__(self, lines):
        super(CopyrightForML, self).__init__(lines)
    
    def put_into_comment(self, text):
        return "/* " + text + " */"

    def is_mark(self, line):
        regex = re.compile(r"^/\*" + Copyright.COPYRIGHT_PATTERN + r"\*/$")
        return (regex.match(line) is not None)
    
    def is_begin(self, line):
        regex = re.compile(r"^<!--" + Copyright.COPYRIGHT_PATTERN + r"\w.*-->$")
        return (regex.match(line) is not None)
        
    def as_str(self):
        copyright_str = "/*\n"
        for line in self.__lines:
            copyright_str += "  " + line
        copyright_str += "*/\n"
        return copyright_str


COPYRIGHT_CLASSES = ( CopyrightForScript,
                      CopyrightForCppOrJava,
                      CopyrightForLaTeX,
                      CopyrightForML,
                      CopyrightForCss, )


def _create_copyright(filename, cr_lines):
    
    # Extract file extension.
    if filename.find(".") >= 0:
        extension = filename.split(".")[-1]
    else:
        extension = ""

    # Create copyright object according to the file extension.
    copyright = None
    for klass in COPYRIGHT_CLASSES:
        if extension in klass.SUPPORTED_EXTENSIONS:
            copyright = klass(cr_lines)
    if copyright is not None:
        logging.debug( "Created copyright object of type %s."
                     % copyright.__class__.__name__ )
    else:
        logging.debug( "Fail to create copyright object for extension %s."
                     % extension )
    return copyright


################################################################################
# utility functions
#

def _read_file(filename):
    text = ""
    try:
        f = open(filename, "r")
        try:
            for line in f:
                text += line.replace("\r", "")
        finally:
            f.close()
    except:
        logging.error("Fail to open file `%s' for read." % filename)
        raise CopyrightError()
    return text

        
def _read_lines(filename):
    lines = []
    try:
        f = open(filename, "r")
        try:
            for line in f:
                lines.append(line)
        finally:
            f.close()
    except:
        logging.error("Fail to open file `%s' for read." % filename)
        raise CopyrightError()
    return tuple(lines)


def _make_backup_filename(filename):
    MAX_COUNTER = 100
    counter = 0
    backup_filename = filename + ".bak"
    while os.access(backup_filename, os.F_OK):
        counter += 1
        if counter <= MAX_COUNTER:
            backup_filename = filename + (".%d.bak" % counter)
        else:
            raise CopyrightError()
    return backup_filename


def _attach_copyright_to_file(filename, copyright, force):
    
    logging.info("Checking file `%s'..." % filename)
    results = []
    to_be_updated = False
    try:
        f = open(filename , "r")
        inside_copyright = False
        try:
            for line in f:
            
                if not inside_copyright:
                    # We are currently outside the copyright segment.
                    if copyright.is_mark(line):
                        # Found copyright mark: insert copyright here.
                        results.append(copyright.as_str())
                        results.append("\n")
                        to_be_updated = True
                    elif copyright.is_begin(line):
                        # Found existing copyright: overwrite or keep?
                        if force:
                            results.append(copyright.as_str())
                            to_be_updated = True
                            inside_copyright = True
                        else:
                            results.append(line)
                    else:
                        # Non-copyright text: keep it unchanged.
                        results.append(line)
                
                else:
                    # We are currently inside the copyright segment.
                    if copyright.is_end(line):
                        # End of existing copyright: append new line.
                        logging.debug("Detected copyright end")
                        results.append("\n")
                        to_be_updated = True
                        inside_copyright = False
        
        except:
            logging.error("Fail to process file `%s'." % filename)
        finally:
            f.close()
    
    except IOError:
        logging.error("Fail to open file `%s'." % filename)
        pass
    
    if len(results) > 0 and to_be_updated:
        backup_filename = _make_backup_filename(filename)
        logging.info( "Updating file `%s' (original file backed up at `%s')..."
                    % (filename, backup_filename) )
        os.rename(filename, backup_filename)
        f = open(filename, "w")
        for line in results:
            f.write(line)
        f.close()


def _attach_copyright(dirname, copyright_filename, force):

    copyright_text = _read_file(copyright_filename)
    logging.info( "Loaded copyright from file `%s':\n\n%s\n"
                % (copyright_filename, copyright_text) )
    copyright_lines = copyright_text.split("\n")
    
    for name in os.listdir(dirname):
        filename = os.path.join(dirname, name)
        if os.path.isfile(filename):
            copyright = _create_copyright(filename, copyright_lines)
            if copyright is not None:
                _attach_copyright_to_file(filename, copyright, force)
            else:
                logging.warning("File `%s' ignored." % filename)
        elif os.path.isdir(filename):
            _attach_copyright(filename, cr_filename, force)


def _create_option_parser():

    parser = OptionParser(usage="""%prog [OPTIONS] DIRECTORY
    Attach copyright to source files under the specified DIRECTORY.""")
    parser.add_option( "-c", "--copyright",
                       dest="copyright",
                       metavar="COPYRIGHT",
                       help="set the copyright file" )
    parser.add_option( "-f", "--force",
                       dest="force",
                       default=False,
                       action="store_true",
                       help="force update copyright" )
    parser.add_option( "-v", "--verbose",
                       dest="verbose",
                       default=False,
                       action="store_true",
                       help="set to verbose mode" )
    parser.add_option( "-d", "--debug",
                       dest="debug",
                       default=False,
                       action="store_true",
                       help="set to debug mode" )
    return parser


def main(argv):

    parser = _create_option_parser()
    (options, args) = parser.parse_args(argv)
    if options.copyright is not None and len(args) == 1:
        if options.debug:
            logging.getLogger().setLevel(logging.DEBUG)
        elif options.verbose:
            logging.getLogger().setLevel(logging.INFO)
        else:
            logging.getLogger().setLevel(logging.ERROR)
        _attach_copyright(args[0], options.copyright, options.force)
    else:
        print >> sys.stderr, "\n"
        if options.copyright is None:
            print >> sys.stderr, "Error: copyright file not specified."
        if len(args) == 0:
            print >> sys.stderr, "Error: directory not specified."
        elif len(args) > 1:
            print >> sys.stderr, "Error: too many directories specified."
        print >> sys.stderr, "\n"
        parser.print_help()


if __name__ == '__main__':
    main(sys.argv[1:])


